Стратегия взаимодействия с клиентами фитнесс - центров "Культурист - Датасаентист"
Описание проекта
Сеть фитнес-центров «Культурист-датасаентист» разрабатывает стратегию взаимодействия с клиентами на основе аналитических данных. Распространённая проблема фитнес-клубов и других сервисов — отток клиентов. Чтобы бороться с оттоком, отдел по работе с клиентами «Культуриста-датасаентиста» перевёл в электронный вид множество клиентских анкет. Наша задача — провести анализ и подготовить план действий по удержанию клиентов.
А именно:
* 1) выделить целевые группы клиентов;
* 2) предложить меры по снижению оттока;
* 3) определить другие особенности взаимодействия с клиентами.
'gender' — пол;'Near_Location' — проживание или работа в районе, где находится фитнес-центр;'Partner' — сотрудник компании-партнёра клуба (сотрудничество с компаниями, чьи сотрудники могут получать скидки на абонемент — в таком случае фитнес-центр хранит информацию о работодателе клиента);Promo_friends — факт первоначальной записи в рамках акции «приведи друга» (использовал промо-код от знакомого при оплате первого абонемента);'Phone' — наличие контактного телефона;'Age' — возраст;'Lifetime' — время с момента первого обращения в фитнес-центр (в месяцах).'Contract_period' — длительность текущего действующего абонемента (месяц, 6 месяцев, год);'Month_to_end_contract' — срок до окончания текущего действующего абонемента (в месяцах);'Group_visits' — факт посещения групповых занятий;'Avg_class_frequency_total' — средняя частота посещений в неделю за все время с начала действия абонемента;'Avg_class_frequency_current_month' — средняя частота посещений в неделю за предыдущий месяц;'Avg_additional_charges_total' — суммарная выручка от других услуг фитнес-центра: кафе, спорттовары, косметический и массажный салон.'Churn' — факт оттока в текущем месяце.# подключение необходимых библиотек
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
import seaborn as sns
from plotly import graph_objects as go
from scipy.cluster.hierarchy import dendrogram, linkage
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, silhouette_score
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.cluster import KMeans
import warnings
warnings.filterwarnings('ignore')
# читаем данные из файла
try:
df = pd.read_csv('gym_churn.csv')
except:
df = pd.read_csv('/datasets/gym_churn.csv')
df.head()
# приведем названия столбцов к нижнему регистру
df.columns = df.columns.str.lower()
# просмотр общей информации о DataFram-e
df.info()
df.duplicated().sum()
df['month_to_end_contract'].unique()
В предоставленных для анализа данных имеются 4000 записей о клиентах. Пропуски отсутствуют. Явных дубликатов не выявлено.
Столбцы gender, near_location, partner, promo_friends, phone, group_visits, churn могут содержать только значения 0 / 1, по смыслу целесообразно привести у типу данных boolean.
Столбец month_to_end_contract целесообразно привести к int, т.к. фактически в нем содержатся только целые числа.
# замена типа данных в столбцах DataFrame
df['month_to_end_contract'] = df['month_to_end_contract'].astype('int')
df[['gender', 'near_location', 'partner', 'promo_friends', 'phone', 'group_visits', 'churn']] = \
df[['gender', 'near_location', 'partner', 'promo_friends', 'phone', 'group_visits', 'churn']].astype('bool')
df.info()
df.describe()
Посмотрев общую информацию о таблице можем сказать следующее:
gender в выборке приблизительно поровну мужчин и женщин;near_location большинство клиентов (порядка 85%) работают или проживают в районе фитнес центра, об этом свидетельствуют значения среднего на уровене 0,84 и первого квартиля 1;partner сотрудников компаний - партнеров и остальных клиентов примерно поровну;promo_friends по акции "приведи друга" занимаются порядка 30% клиентов;phone контактный телефон предоставили порядка 90% клиентов;contract_period средняя длительность действующего абонемента составляет 4-5 месяцев в 25% случаев клиенты покупали более длительные абонементы (от 6 до 12 месяцев);group_visits групповыми тренировками пользуются менее половины клиентов, можно предположить, что порядка 25 - 40%;age наш фитнес центр посещают клиенты в возрасте от 18 лет до 41 года, средний возраст нашего клиента 29 лет;avg_additional_charges_total средняя суммарная выручка от дополнительных услуг составляет 136 - 146 денег (единица измерения нам неизвестна);month_to_end_contract в среднем у клиентов остаток срока абонемента наблюдаем на уровне 4 месяцев, однако у больше половины клиентов остался всего 1 оплаченный месяц занятий;lifetime у нас имеются данные о клиентах, которые обращались в фитнес центр впервые от 0 до 31 месяца назад, среднее значение здесь наблюдаем на уровне 3-4 месяцев;avg_class_frequency_total можно сказать, что в среднем за неделю клиенты посещают клуб дважды, однако есть и большие любители фитнеса, занимающиеся 6 раз в неделю;avg_class_frequency_current_month данные о среднем количестве посещений на горизонте 1 месяца мало отличаются от данных за весь период;churn тех кто продолжает пользоваться услугами фитнес центра очевидно больше в среднем ушли менее 25% клиентов (1 - клиент ушел, 0 - продолжает пользоваться).Далее посмотрим средние значения в разбивке по оставшимся и ушедшим клиентам.
round (df.groupby('churn').mean(), 2)
Выделим наиболее интересные наблюдения по средним значениям в полученных выборках:
Далее построим гистограммы и распределения признаков для тех, кто ушел (отток) и тех, кто остался
sns.set()
fig, ax_lst = plt.subplots(1, 1)
for i in df.drop('churn', axis=1).columns:
sns.distplot(df.loc[df['churn'] == 0, i], label='остались')
sns.distplot(df.loc[df['churn'] == 1, i], label='ушли')
plt.ylabel('Количество клиентов')
plt.xlabel('Значение признака')
plt.grid()
plt.title(f'{i}')
plt.legend()
plt.show()
fig, axs = plt.subplots(len(df.columns) // 2, 2)
fig.set_size_inches(10, 16)
fig.set_dpi(300)
new_axs = [item for sublist in axs for item in sublist]
for i, column in enumerate(df.columns):
sns.histplot(data=df, x=column, hue='churn', stat='density', common_norm=False, ax=new_axs[i])
new_axs[i].set_title('Распределения признака {}'.format(column))
plt.tight_layout()
plt.show()
Выделим основные различия, которые видим на основании графиков меньший отток демонстрируют:
Не проявляют лояльность и чаще попадают в отток
Не наблюдается разницы и отток сопоставим
В большинстве случаев распределения можно считать нормальными. За исключением, пожалуй, распределений по продаже доп. услуг, времени с момента первого обращения и среднего числа посещений за последний месяц.
Далее посмотрим данные матрицы корреляций
df.corr()
plt.figure(figsize=(15, 15))
sns.heatmap(df.corr(), annot = True, fmt='.1g', vmin=-1, vmax=1, center= 0, cmap= 'coolwarm', linewidths=1, linecolor='black')
plt.title('Тепловая карта по признакам')
plt.xticks(fontsize=14, rotation=80)
plt.yticks(fontsize=14)
plt.ylabel('Признаки')
plt.xlabel('Признаки');
contract_period и month_to_end_contract, avg_class_frequency_current_month и avg_class_frequency_total, что само по себе логично и очевидно; Поскольку для построения модели прогнозирования не желательно наличие сильно скоррелированных показателей, для построения модели прогнозирования оттока решаю убрать из выборки такие признаки.
df_for_model = df.drop('avg_class_frequency_total', axis=1)
df_for_model = df.drop('month_to_end_contract', axis=1)
#df = df.drop('contract_period', axis=1)
#df = df.drop('avg_class_frequency_current_month', axis=1)
Вывод:
На данном шаге мы провели предобработку данных и исследовательский анализ EDA. Выявили для каких клиентов в большей степени характерен отток.
категориальные переменные уже имеют числовой вид
нет признаков, коррелирующих с целевой переменной;
Выделение обучающей и валидационной выборок
# разделим наши данные на признаки (матрица X) и целевую переменную (y)
X = df_for_model.drop('churn', axis=1)
y = df_for_model['churn']
# разделяем модель на обучающую и валидационную выборку
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
Стандартизация данных
# создадим объект класса StandardScaler и применим его к обучающей выборке
scaler = StandardScaler()
X_train_st = scaler.fit_transform(X_train) #обучаем scaler и одновременно трансформируем матрицу для обучающей выборки
#применяем стандартизацию к матрице признаков для тестовой выборки
X_test_st = scaler.transform(X_test)
Обучение модели на train-выборке двумя способами: логистической регрессией, случайным лесом.
# задаем алгоритм для нашей модели, сначала Логистическая регрессия
model_LR = LogisticRegression(solver='liblinear', random_state=0)
# обучение модели
model_LR.fit(X_train_st, y_train)
# воспользуйтесь уже обученной моделью, чтобы сделать прогнозы
predictions_LR = model_LR.predict(X_test_st)
probabilities_LR = model_LR.predict_proba(X_test_st)[:,1]
model_RF = RandomForestClassifier(random_state=0)
model_RF.fit(X_train_st, y_train)
predictions_RF = model_RF.predict(X_test_st)
probabilities_RF = model_RF.predict_proba(X_test_st)[:,1]
Оценка метрики accuracy, precision и recall для обеих моделей на валидационной выборке. Сравнение по ним модели.
print ('Значениея метрик для модели логистической регресси')
print ('accuracy = ', round(accuracy_score(y_test, predictions_LR), 2))
print ('precision = ', round(precision_score(y_test, predictions_LR), 2))
print ('recall = ', round(recall_score(y_test, predictions_LR), 2))
accuracy/точность: Доля правильных ответов на уровне 0.92, отличный показатель.
Метрики без привязки к балансу "классов" также показывают неплохие результаты
precision/точность (доля правильных ответов только среди целевого класса) 0,85
recall/полнота (сколько реальных объектов класса мы смогли обнаружить с помощью модели) 0,83
print ('Значениея метрик для модели случайный лес')
print ('accuracy = ', round(accuracy_score(y_test, predictions_RF), 2))
print ('precision = ', round(precision_score(y_test, predictions_RF), 2))
print ('recall = ', round(recall_score(y_test, predictions_RF), 2))
В данной модели значение аналогичных метрик сопоставимы и уступают логистической регрессии на 1-2 сотых.
Вывод:
На данном было построено две модели прогнозирования оттока клиентов.
и оценены их качество для наших данных с помощью метрик accuracy (доля правильных ответов), precision (точность), recall (полнота). По всем метрикам получаем не плохие значения у обоих моделей, но чуть лучше демонстрирует себя Логистическая регрессия .
На основании полученных значений метрик, можно сказать, что обе модели показывают себя неплохо и способны делать приемлемые прогнозы.
Для начала отбросим столбец с оттоком и стандартизируем данные.
X = df.drop('churn', axis=1)
# стандартизируем данные
sc = StandardScaler()
X_sc = sc.fit_transform(X)
# в переменной linked сохранена таблица «связок» между объектами, её можно визуализировать как дендрограмму
linked = linkage(X_sc, method = 'ward')
# строим дендрограмму
plt.figure(figsize=(15, 10))
dendrogram(linked, orientation='top')
plt.title('Hierarchial clustering')
plt.show()
Можем предположить, что данные целесообразно разделить на 4 кластера. Согласно условию задачи, установим число кластеров равным 5 для дальнейшего исследования.
km = KMeans(n_clusters=5, random_state=0) # задаём число кластеров, равное 5, и фиксируем значение random_state для воспроизводимости результата
labels = km.fit_predict(X_sc) # применяем алгоритм к данным и формируем вектор кластеров
# добавляем в исходные данные метки с номерами кластеров
df['cluster_km'] = labels
df.head()
Посмотрим средние значения метрик для каждого кластера.
round (df.groupby('cluster_km').mean().T, 2)
Посмотрим сколько клиентов попало в каждый кластер
df.groupby('cluster_km')['gender'].count()
Получили кластеры размерностью от 385 до 1262.
Оценим качество проведенной кластеризациии с помощью метрики Silhouette_score
print('Silhouette_score: = ', silhouette_score(X_sc, labels))
Получаем значение метрики на уровне 0,14, это означает, что возможна лучшая кластеризация чем у нас получилась.
Самые лояльные клиенты попали в кластер 0 и кластер 4 (отток на уровне от 3 до 7 %), худшие показатели оттока в кластерах 2 и 3 (44% и 51%) соответственно.
Можно отметить, что в группу с минимальным оттоком в основном попали клиенты со средним возрастом 30 лет, проживающие рядом с фитнес центром, тратящие в среднем больше остальных на доп. услуги. Примерно половина из них посещает групповые занятия, многие (около 78% являются сотрудниками компаний - партнеров. Посещают зал эти клиенты 2 раза в неделю. Являются владельцами годовых абонементов и прозанимались 2 месяца из них.
В кластере где собралось много отточных клиентов относительно мало участников партнерских и "дружеских" программ, более молодые клиенты (около 28), клиенты с краткосрочным абонементом реже остальных посещающие зал.
Полученные средние в разбивке по кластерам значения не противоречат сделанным ранее наблюдениям.
На данном шаге можно предположить, что и по итогам исследования кластер 0 покажет лучшие результаты относительно общего оттока в выборке и его метрики будут основными для построения портрета нашего клиента.
Далее построим распределения признаков по кластерам. Достаточно наглядно на мой взгляд для булевых значений и срока абонемента распределения иллюстрируют столбчатые графики, для остальных величин boxplot.
# Построим график для логических и дискретных величин
values = ['gender', 'near_location', 'partner', 'promo_friends', 'phone', 'contract_period',\
'group_visits', 'churn']
fig, ax = plt.subplots()
for column in values:
plt.title(column)
sns.countplot(data=df, x=column, hue='cluster_km')
plt.xlabel('Распределение')
plt.ylabel('Количество клиентов')
plt.legend(bbox_to_anchor=(1, 1))
plt.show()
values = ['age', 'lifetime', 'avg_class_frequency_total', 'avg_class_frequency_current_month', 'avg_additional_charges_total',\
'month_to_end_contract']
fig, ax = plt.subplots()
for column in values:
plt.title(column)
sns.boxplot(data=df, x='cluster_km', y=column)
plt.xlabel('кластер')
plt.ylabel('значение '+column)
plt.show()
Для всех кластеров количество мужчин и женщин разделилось почти поровну. Акционные клиенты (партнеры, "приведи друга") менее отточны за исключением кластера 0.
Выделим основные маркеры характерные для наших кластеров по итогам построения графиков распределения:
Кластер 0:
В кластер 0 попали преимущественно следующие клиенты
Кластер 1:
В кластер 1 попали преимущественно следующие клиенты
Кластер 2:
В кластер 2 попали преимущественно следующие клиенты
Кластер 3:
В кластер 3 попали преимущественно следующие клиенты
Кластер 4:
В кластер 4 попали преимущественно следующие клиенты
Теперь посчитаем долю оттока в каждом кластере относительно общего числа оттоков в выборке.
part_churn = df.query('churn==1').groupby('cluster_km')['churn'].count() / df.query('churn==1')['churn'].count()
fig = go.Figure(data=[go.Pie(labels=part_churn.index, values=part_churn.values, title='Доля оттока в кластере')]);
fig.show() ;
part_churn
Вывод
Как и предполагалось Кластер 0 показывает лучшие результаты по оттоку клиентов 2,64%, не плохие показатели у кластера 4 на уровне 5,37%. Ключевые черты характерные для этих кластеров можно использовать для построение портрета нашего "идеального" лояльного клиента.
Это человек в возрасте около 30 лет, проживающий рядом, посещающий зал 2 иногда 3 раза в неделю, любитель групповых занятий и пользующийся доп услугами, владелец годового абонемента, чаще всего сотрудник компании-партнера.
Худшие показатели у кластеров 3 и 2 (61,2% и 21,1% соответственно), аналогично ключевые черты характерные для этих кластеров можно использовать для построение портрета нашего потенциально отточного клиента.
Это человек моложе 30 лет, проживающий далеко от клуба, посещающий клуб не чаще раза в неделю, владелец краткосрочного абонемента, не склонный к групповым занятиям.
В данной работе мы проанализировали данные клиентов фитнес центра. На основании имеющихся данных было построено две модели прогнозирования оттока клиентов.
Основываясь на показаниях ключевых метрик оценки моделей accurancy, precision и recall можно сказать, что обе модели хорошо показывает себя в прогнозировании оттока. По совокупности чуть лучше показывает себя модель Логистическая регрессия .
Далее была проведена кластеризация клиентов. Построенная дендрограмма показала 4 явных кластера, согласно заданию, выборку разделили на 5 кластеров. Построили распределение признаков по кластерам, на основании которых построили портрет лояльного и ненадежного клиента.
Основные числовые характеристики и портреты даны по ходу исследования. Здесь сфокусируемся на рекомендациях отделу маркетинга.
Рекомендации отделу маркетинга
Проводить маркетинговые активности, направленные на целевую аудиторию:
Работа с действующими клиентами:
Тарифная политика:
Работа со склонными к оттоку клиентами:
Акции: